#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4
#
# Courier filter that normalizes From: headers to a canonical form:
# From: cn <mail>
# Author: Aleksander Adamowski <aleksander.adamowski@olo.org.pl>
# Based-on: Pawel Sawicki's isoCourierFilter (Pawel J. Sawicki <p-j-s@p-j-s.name>)
# s/-//g
# $Id: fromNormalizer.py 60 2007-07-19 10:43:40Z olo $
#
############################################################################
#                                                                          #
#   This program is free software; you can redistribute it and/or modify   #
#   it under the terms of the GNU General Public License as published by   #
#   the Free Software Foundation; either version 2 of the License, or      #
#   (at your option) any later version.                                    #
#                                                                          #
############################################################################

import courier.control
import courier.xfilter
import ldap
import re
import codecs
import time
import sys
import os
import os.path
import stat
import email
from email.MIMEText import MIMEText
from email.Header import Header
import pickle
import thread

# recipient domains exempt from normalizing:
exemptRecipientDomains              = ['exemptrecip.example.com']
reExemptRecipientDomain = re.compile("[^@]+@("+("|".join(exemptRecipientDomains))+")", re.I)

# sender domains exempt from normalizing:
exemptSenderDomains                 = ['exemptsender.example.com']
reExemptSenderDomain                = re.compile("[^@]+@("+("|".join(exemptRecipientDomains))+")", re.I)
reNonAlnumChars                     = re.compile('[^a-zA-Z0-9]')

# LDAP specific data
ldapServerURI       = 'ldap://localhost:389'                # server
ldapBaseDN          = 'ou=People,o=Example'                 # base DN
ldapSearchScope     = ldap.SCOPE_SUBTREE                    # search scope
ldapAttributes      = ['dn', 'mail', 'altkomPermittedSenderAddresses', 'cn', 'telephoneNumber', 'mobile' ] # returned attributes
ldapFilter          = '(&(uid=%(uid)s)(!(ou=others)))'        # filter
ldapMailAttributes  = ['uid']
ldapMailFilter      = '(&(mail=%(mail)s)(!(ou=others)))'        # filter
ldapBindDN          = ''
ldapBindPassword    = ''
ldapRetries         = 8

# log debug messages
debug_logging       = True

# connect to ldap server
ldapConnection      = ldap.initialize(ldapServerURI)
ldapMutex           = thread.allocate_lock()

# bind synchronously
ldapConnection.simple_bind_s(ldapBindDN, ldapBindPassword)

# say hello
sys.stderr.write('Initialized the "fromNormalizer" python filter.\n')

def doFilter(bodyFile, controlFileList):
        """
        Get the message from the queue, but if it has already been processed,
        stop processing and leave it alone:
        """
        try:
            msgfilter = courier.xfilter.XFilter('fromNormalizerFilter', bodyFile,
                controlFileList)
        except courier.xfilter.LoopError, e:
            debug_log('Message already filtered, finishing work and letting other filters continue.')
            # LoopError indicates that we've already filtered this message.
            return ''

        msg = msgfilter.getMessage()

        # get the authenticated user:
        authUser = determine_auth_user(controlFileList, bodyFile, msg)
        if authUser is None:
            debug_log('No authentication data, finishing work and letting other filters continue.')
            return ''

        """
        Check if all recipients' adresses belong to an exempt recipient domain.
        If at least one non-exempt address is present in the recipient table -
        append the signature:
        """
        isNonExemptRecipientPresent = False

        # get the recipients table
        recipients = courier.control.getRecipients(controlFileList)

        # find any non-exempt adresses
        for recipient in recipients:
            if not reExemptRecipientDomain.match(recipient.lower()):
                #debug_log('non exempt recipient found: ' + recipient)
                isNonExemptRecipientPresent = True


        """
        If all recipients are in exempt domains, stop processing:
        """
        if not isNonExemptRecipientPresent:
            debug_log('no non-exempt recipients, finishing work and letting other filters continue.')
            return ''

        # prepare local ldap filter
        localLdapFilter = ldapFilter % { 'uid': authUser }

        (ldapResultType, ldapResultData) = threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes)

        if len(ldapResultData) != 1 or not ldapResultType == ldap.RES_SEARCH_RESULT:
            # user hasn't been found in the LDAP directory
            debug_log('no LDAP entry or ambiguous search result for %s.' % localLdapFilter)
            debug_log(pickle.dumps(ldapResultData))
            debug_log('Finishing work and letting other filters continue.')
            return '' 

        ldapEntryAttrs = ldapResultData[0][1]
        canonicalAddress = ''
        if 'mail' in ldapEntryAttrs:
            canonicalAddress = ldapEntryAttrs['mail'][0].lower()
            debug_log('Canonical address :' + canonicalAddress)
        else:
            debug_log('No "mail" attribute for %s! Finishing work and letting other filters continue.' % authUser)
            return ''


        """
        Check if the sender isn't exempted from processing.
        If he is, stop processing:
        """
        # get the sender
        #emailSender = courier.control.getSender(controlFileList).lower()
        emailSender = canonicalAddress
        #debug_log('sender: ' + emailSender)

        if reExemptSenderDomain.match(emailSender):
            debug_log('sender is exempt, finishing work and letting other filters continue.')
            return ''


        # get the message

        fromSender = msg.get_all('from')
        debug_log('fromSender: %s' % fromSender)
        (mailSenderRealName, mailSenderEmail) = email.Utils.getaddresses(fromSender)[0]
        newMailSenderRealName = []
        for elem in email.Header.decode_header(mailSenderRealName):
            if not elem[1] is None:
                newMailSenderRealName.append(unicode(elem[0], elem[1]))
            else:
                newMailSenderRealName.append(unicode(elem[0], 'us-ascii'))
        mailSenderRealName = unicode(" ".join(newMailSenderRealName))
        debug_log('Senders real name: %s, e-mail: %s' % (mailSenderRealName, mailSenderEmail))

        senderAddrOK = True
        senderRealNameOK = True

        if mailSenderEmail.lower() == canonicalAddress.lower():
            debug_log(mailSenderEmail + ' is a permitted sender address.')
            senderAddrOK = True
        else:
            """
            altkomPermittedSenderAddresses attribute, if present, designates addresses which the sender
            is permitted to send mail from:
            """
            if 'altkomPermittedSenderAddresses' in ldapEntryAttrs:
                altkomPermittedSenderAddresses = ldapEntryAttrs['altkomPermittedSenderAddresses']
                if mailSenderEmail in altkomPermittedSenderAddresses or mailSenderEmail == canonicalAddress:
                    debug_log(mailSenderEmail + ' is a permitted sender address.')
                    senderAddrOK = True
                else:
                    senderAddrOK = False
            else:
                debug_log('no altkomPermittedSenderAddresses found for %s and sender address %s is not canonical.' % (localLdapFilter, mailSenderEmail))
                senderAddrOK = False

        if 'cn' in ldapEntryAttrs:
            if mailSenderRealName.lower() in [unicode(elem, 'utf-8').lower() for elem in ldapEntryAttrs['cn']]:
                debug_log(mailSenderRealName + ' is a permitted sender real name.')
                senderRealNameOK = True
            else:
                debug_log('Real name "%s" is not OK for sender %s, should be in "%s".' % (mailSenderRealName.lower(), localLdapFilter, " ".join([unicode(elem, 'utf-8').lower() for elem in ldapEntryAttrs['cn']])))
                senderRealNameOK = False
        else:
                debug_log('no canonical name found for sender %s!!!' % localLdapFilter)

        if (not senderAddrOK) or (not senderRealNameOK):
            if (not senderAddrOK):
                correctedSenderAddr = canonicalAddress

            else:
                correctedSenderAddr = mailSenderEmail
            if (not senderRealNameOK):
                correctedSenderRealName = unicode(ldapEntryAttrs['cn'][0], 'utf-8')
            else:
                correctedSenderRealName = mailSenderRealName
            #debug_log('correcting sender to: "%s", <%s>' % (unicode(correctedSenderRealName,'utf-8'), correctedSenderAddr))
            correctedSenderRealName = str(Header(correctedSenderRealName, 'utf-8'))
            correctedFrom = email.Utils.formataddr((correctedSenderRealName, correctedSenderAddr))
            msg.replace_header('From', correctedFrom)
            debug_log('correcting sender to: "%s"' % correctedFrom)
            #correctedFromHeader = Header(unicode(correctedFrom), 'utf-8')
            #msg.replace_header('From', correctedFromHeader.encode())

        msgfilter.submit()
        # Return 250, no more filters should be run on this copy.
        debug_log('ending with 050 OK, no more filters should be run on this copy.')
        return '050 OK'

def debug_log(message):
    if debug_logging:
        sys.stderr.write((__name__ + ': ' + message + '\n').encode('utf-8'))

def threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes):
    global ldapConnection
    global ldapMutex
    ldapMutex.acquire()
    try:
        for tryNumber in range(ldapRetries):
            if tryNumber == ldapRetries:
                debug_log('Giving up after %d LDAP reconnection attempts.' % ldapRetries)
                return ''
            try:
                # get the signature template id from LDAP:
                ldapResultId = ldapConnection.search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes)
                ldapResultType, ldapResultData = ldapConnection.result(ldapResultId)
                break
            except ldap.LDAPError, error_message:
                debug_log('LDAP operation error on try %d out of %d: %s' % (tryNumber + 1, ldapRetries, error_message))
                time.sleep(1)
                if tryNumber != ldapRetries:
                    debug_log('retrying LDAP operation')
                try:
                    ldapConnection = ldap.initialize(ldapServerURI)
                    ldapConnection.simple_bind_s(ldapBindDN, ldapBindPassword)
                except ldap.LDAPError, error_message:
                    debug_log('LDAP reconnection error on try %d out of %d: %s' % (tryNumber + 1, ldapRetries, error_message))
    finally:
        ldapMutex.release()

    if ldapMutex.locked():
        ldapMutex.release()
        debug_log('Unreleased ldapMutex detected!')
    return (ldapResultType, ldapResultData)

def mail2uid(mailAddress):
    localLdapFilter = ldapMailFilter % { 'mail': mailAddress }
    debug_log('Searching for auth based on 127.0.0.1 sender: ' + localLdapFilter)
    (ldapResultType, ldapResultData) = threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapMailAttributes)
    if len(ldapResultData) != 1 or not ldapResultType == ldap.RES_SEARCH_RESULT:
        debug_log('no LDAP entry or ambiguous search result for %s.' % localLdapFilter)
        debug_log(pickle.dumps(ldapResultData))
        return None
    else:
        mailUID = ldapResultData[0][1]['uid'][0]
        debug_log('Found 127.0.0.1 authUser: ' + mailUID)
        return mailUID

def determine_auth_user(controlFileList, bodyFile, msg):
    authUser = courier.control.getAuthUser(controlFileList, bodyFile)
    if authUser:
        if msg.has_key('X-Pythonfilters-Auth'):
            msg.replace_header('X-Pythonfilters-Auth', authUser)
        else:
            msg.add_header('X-Pythonfilters-Auth', authUser)
    else:
        if msg.has_key('X-Pythonfilters-Auth'):
            authUser = msg.get('X-Pythonfilters-Auth')
        else:
            if courier.control.getSendersIP(controlFileList) == '127.0.0.1':
                """
                Typical webmail installations don't use SMTP AUTH, but the sender address
                in such cases can usually be trusted and searched on.
                """
                authUser = mail2uid(courier.control.getSender(controlFileList))
            else:
                return None
    return authUser

